#ifndef TM1638_H_
#define TM1638_H_

#include <inttypes.h>
#include <stdbool.h>
#include "driver/gpio.h"
#include "rom/ets_sys.h"
#include "freertos/FreeRTOS.h"
// #include "tm1638_Key.h"

// 数码管位置映射类型
typedef struct
{
    uint8_t idx[8];
} segLayout_t;

// 键盘缓冲区类型
typedef struct
{
    uint8_t buf[4];
} keyBuf_t;

class TM1638
{
private:
    bool m_useCommonAnode = false;       // 为true则是共阳极数码管，这个比较扯。
    const static size_t m_kSegCount = 8; // 模块最大数码管数量，一般就是8.
    // 数码管模块实际索引映射，给不按常理出牌的神经病硬件模块用。
    // 正常模块按照从左到右0..7布局，神经病按其他布局，比如从右到左0..7。
    uint8_t m_segLayout[m_kSegCount];

    gpio_num_t m_pinClk;  // clk pin
    gpio_num_t m_pinDta;  // data pin
    gpio_num_t m_pinStb;  // stb pin
    uint8_t m_brightness; // 亮度值

    // 共阴极和共阳极显示数据。
    // 并非和TM1638一一对应的显示寄存器映像，这里是程序内使用的显示数据。
    uint8_t m_dispDatas[m_kSegCount];

    // 用于接收键盘底层读来的4字节数据.
    keyBuf_t m_keyBuf = {.buf = {0x00}};
    uint32_t m_raw; // 4个键盘寄存器值组合成的真值   

    // 键盘事件环形缓冲区
    // key_event_t m_keyFifo[KEY_BUF_SIZE] = {{.key_id = 0, .type = KEY_EV_INIT}};
    // uint8_t m_keyWi = 0; // key_fifo的写索引
    // uint8_t m_keyRi = 0; // key_fifo的读索引

    // 总线互斥锁.
    SemaphoreHandle_t m_busMux;

    // 键盘任务句柄，用于判断键盘任务是否已经激活，避免多次启动造成崩溃。
    TaskHandle_t m_keyTaskHandle = nullptr;

    // 定义几个命令
    enum Cmd
    {
        CMD_ADD_AUTO = 0X40,   // 自动地址增加模式命令
        CMD_ADD_FIX = 0X44,    // 固定地址模式命令
        CMD_RD_KEY = 0X42,     // 读键盘命令
        CMD_START_ADDR = 0xC0, // 显存开始地址
        CMD_BRIGHT_ON = 0x88,  // 亮度打开
        CMD_BRIGHT_OFF = 0x80, // 亮度关闭
    };

    inline void delay()
    {
        ets_delay_us(2);
    };

    inline void stbLow()
    {
        gpio_set_level(m_pinStb, 0);
        delay();
    }
    inline void stbHight()
    {
        gpio_set_level(m_pinStb, 1);
        delay();
    }

    // 发送一个字节的数据
    void sendByte(uint8_t byte);
    // 读取一个字节的数据
    uint8_t readByte();
    // 发送命令
    void sendCommand(uint8_t cmd);
    // 发送一个(地址-值)对。
    void sendAddrData(uint8_t addr, uint8_t data); // 向指定地址发送一个字节的数据.

    // 读键盘
    // 读键盘和显示必须分时进行.
    void scanKey();

    // 使用地址自动增加模式发送一序列数据显示.
    // data最多16个字节，因为TM1638只有16个显示寄存器。
    // 这是面向芯片的功能，实际模块上奇数显示地址上并没有接7段数码管，因此data内的数据并不等于显示的数据。
    // 也就是data数据需要包含待显示的数据和并不需要显示的奇数地址的数据。
    void setSegmentAuto(const int8_t dispReg_start_addr, const uint8_t *data, int arrQty);

    // 使用固定地址模式向指定显示寄存器地址写入一个字节的数据。
    // 地址指的是TM1638显示寄存器地址，实际模块上只有偶数地址接了数码管，奇数地址并没有数码管。
    void setSegmentFixed(const int8_t dispReg_addr, const uint8_t data);

    // 显示寄存器复位
    inline void resetReg()
    {
        for (int i = 0; i < sizeof(m_dispDatas) / sizeof(m_dispDatas[0]); i++)
        {
            m_dispDatas[i] = 0x00;
        }
    }

    // 取得关于str字符串的映像，映像大小最好限定为12个字节。
    // 必须传入image的实际容量大小arrSize。
    static void getImage(const char *str, uint8_t *image, size_t arrSize);

    // 把共阴极映像转换为共阳极映像
    void convertCathode2Anode();

    // 关于键盘的扩展部分
    // 静态跳板，签名符合 FreeRTOS的任务回调函数
    // static void keyTaskStaticTrampoline(void *param);
    // 真正的任务循环
    // void keyTaskLoop();

public:
    // TM1638() {};
    TM1638(gpio_num_t pin_stb, gpio_num_t pin_clk, gpio_num_t pin_dta, uint8_t brightness, const segLayout_t *p, bool anode = false);
    ~TM1638();

    /**
     * @brief 设置亮度
     *
     * @param level 亮度等级0-7
     */
    inline void setBrightness(uint8_t level) { m_brightness = level;}

    /**
     * @brief 在指定数码管上显示数字
     * 不会主动清屏，提供在原来的显示基础上修改指定索引数码管的数据。
     * @param segment_idx 数码管索引
     * @param num 待显示的数字，0-9
     * @param dot 是否需要小数点
     */
    void dispNumberOneSegment(const int8_t segment_idx, const uint8_t num, const bool dot = false);

    /**
     * @brief 显示字符串
     * 该函数不会主动清显示寄存器，如果有必要，请在调用前clear().
     * @param str 待显示的字符串，一般是处理好的数字字符串。如果是小数，则str不含小数点但指定小数点位置。
     * @param dot_idx 小数点所在字符串位置的前一位索引。如果需要显示小数点，则str本身应该不含小数点。
     * @param left 是否左对齐
     */
    void dispString(const char *str, int dot_idx = -1, bool left = false);

    /**
     * @brief 从右向左滑动显示字符串
     *
     * @param str
     * @param dot_idx 小数点所在字符串位置的前一位索引。如果需要显示小数点，则str本身应该不含小数点。
     */
    void dispStringSlide(const char *str, int dot_idx = -1);

    /**
     * @brief 显示数字:整数
     * 。
     * @param num
     * @param slide 如果silid=true，则跑马灯显示，否则只显示数码管位数的数字，之后的被截断。
     */
    void dispNum(const int num, bool slide = false);

    /**
     * @brief 显示数字：浮点数
     * 规定：只有带小数点的数码管才能显示小数点，不采用用symbols.h中定义的短划线。
     * @param num
     * @param slide 如果silid=true，则跑马灯显示，否则只显示数码管位数的数字，之后的被截断。
     */
    void dispNum(const double num, int precision = 6, bool slide = false);

    // 清屏
    void clear();

    /**
     * @brief 读键盘
     * 提供外部直接操控键盘应用的能力.可以不使用startKeyTask()来启动键盘任务，而是自己处理键盘应用。
     * 如果只需要4字节键值，就不用传入ptrBuf。keyBuf_t也是四字节键值，不过是4个单独的字节结构。
     * @param ptrBuf 指向keyBuf_t类型的指针
     * @return uint32_t 返回4字节键值。
     */
    inline uint32_t readKey(keyBuf_t* ptrBuf = nullptr)
    {
        scanKey();
        m_raw = m_keyBuf.buf[0] | m_keyBuf.buf[1] << 8 | m_keyBuf.buf[2] << 16 | m_keyBuf.buf[3] << 24;
        if (ptrBuf != nullptr)
        {
            ptrBuf->buf[0] = m_keyBuf.buf[0];
            ptrBuf->buf[1] = m_keyBuf.buf[1];
            ptrBuf->buf[2] = m_keyBuf.buf[2];
            ptrBuf->buf[3] = m_keyBuf.buf[3];
        }            
        return m_raw;
    };

    // 获取键盘环形队列中的事件:出队
    // 如果队列有数据则返回true，没有数据则返回false.
    // ev是有数据时的键盘事件，也就是队列值。
    // bool getKeyEvent(key_event_t *ev);

    // 启动键盘任务
    // prio：优先级
    // core: 任务运行在哪个核心.
    // 创建键盘任务，任务处理函数是跳板函数static void keyTaskStaticTrampoline(void *pv).
    // 该跳板的功能是拿回实例并转向实例的键盘循环函数.
    // bool startKeyTask(UBaseType_t prio = 3,
    //                   BaseType_t core = 1);
};

#endif